<?php

namespace Fize\Third\FuTu;

use Fize\Net\Socket;
use InvalidArgumentException;
use RuntimeException;

abstract class Common
{

    /**
     * @var string 协议监听地址
     */
    protected $host = '127.0.0.1';

    /**
     * @var string 协议监听端口
     */
    protected $port = '11111';

    /**
     * @var Socket Socket对象
     */
    protected $socket;

    /**
     * @var int 请求的序列号
     */
    private $sequence = 0;

    public $connID = 0;

    public $loginUserID = 0;

    /**
     * 初始化的返回
     * @var int
     */
    protected $serverVer = 0;

    /**
     * 初始化接口返回的加密密码
     * @var string
     */
    public $connAESKey = '';

    /**
     * 初始化的返回,心跳间隔
     * @var int
     */
    protected $keepAliveInterval = 5;

    /**
     * AES加密通信CBC加密模式的iv,固定为16字节长字符串
     * @var string
     */
    protected $aesCBCiv = '';

    /**
     * 心跳包定时器
     * @var int
     */
    private $timer = 0;

    /**
     * 同一个连接只解锁一次
     * @var string
     */
    public $unlock = null;

    /**
     * 构造
     * @param string|null $host 协议监听地址
     * @param string|null $port 协议监听端口
     */
    public function __construct(string $host = null, string $port = null)
    {
        if ($host) {
            $this->host = $host;
        }
        if ($port) {
            $this->port = $port;
        }
        $this->connect();
    }

    /**
     * 析构
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * 获取股票简易代码
     *
     * 简易股票代码指只保留数字部分的股票代码
     * @param string $code 股票代码
     * @return string
     */
    protected static function getShortCode(string $code): string
    {
        return preg_replace("/[^0-9]/", "", $code);
    }

    /**
     * 获取股票所属行情市场
     * @param string $code 股票代码
     * @return int
     */
    protected static function getMarket(string $code): int
    {
        $code = self::getShortCode($code);
        if (preg_match('/^(58|60|68|70|73|90)([0-9]{4})$/', $code, $matches)) {  // 上海证券交易所(含科创板)
            $market = 21;
        } elseif (preg_match('/^(00|03|08|20|30)([0-9]{4})$/', $code, $matches)) {  // 深圳证券交易所(含创业板)
            $market = 22;
        } elseif (preg_match('/^(43|82|83|87|88)([0-9]{4})$/', $code, $matches)) {  // 北京证券交易所
            $exchange = 'BJ';
            throw new InvalidArgumentException("不支持的股票代码格式:$exchange");
        } elseif (preg_match('/^(40)([0-9]{4})$/', $code, $matches)) {  // 新三板(北京证券交易所)
            $exchange = 'NQ';
            throw new InvalidArgumentException("不支持的股票代码格式:$exchange");
        } else {
            throw new InvalidArgumentException("不支持的股票代码格式");
        }
        return $market;
    }

    /**
     * 连接
     */
    private function connect()
    {
        if ($this->socket === null) {
            $this->socket = new Socket();
            $this->socket->create(AF_INET, SOCK_STREAM, SOL_TCP);
            $this->socket->setOption(SOL_SOCKET, SO_SNDBUF, 2048);
            $this->socket->setOption(SOL_SOCKET, SO_RCVBUF, 2048);

            if (!$this->socket->connect($this->host, $this->port)) {
                throw new RuntimeException(Socket::strerror($this->socket->lastError()));
            }
        }
        if ($this->timer && (time() - $this->timer >= $this->keepAliveInterval)) { //每N秒发一次心跳(只有同步模式会初始化时间)
            $this->timer = time(); // 锁住
            $this->KeepAlive();
        }
    }

    /**
     * 关闭连接,销毁之前的数据
     */
    private function close()
    {
        if ($this->socket) {
            $this->socket->close();
        }
        $this->socket = null;
        $this->connID = 0;
        $this->unlock = false;
    }

    /**
     * 初始化连接
     * @return string
     */
    protected function InitConnect(): string
    {
        if ($this->connID) { //已经初始化过
            return $this->connID;
        }
        $C2S = [
            "clientVer"           => 0,
            'clientID'            => '0',
            'recvNotify'          => false,
            'packetEncAlgo'       => -1, //0富途修改过的AES的ECB加密模式;-1不加密;1标准的AES的ECB加密模式;2标准的AES的CBC加密模式
            'pushProtoFmt'        => 1, //0:pb,1:json
            'programmingLanguage' => ''
        ];
        $ret = $this->send(1001, $C2S);
        $this->serverVer = (int)$ret['serverVer'];
        $this->loginUserID = (string)$ret['loginUserID']; //uint64
        $this->connID = (string)$ret['connID']; //uint64
        $this->connAESKey = (string)$ret['connAESKey'];
        $this->keepAliveInterval = (int)$ret['keepAliveInterval'];
        $this->aesCBCiv = (string)$ret['aesCBCiv'];

        return $this->connID;
    }

    /**
     * 心跳保活
     * @return int
     */
    protected function KeepAlive(): int
    {
        if (!$this->InitConnect()) {
            return 0;
        }
        $C2S = [
            'time' => time(),
        ];
        if (!$ret = $this->send(1004, $C2S)) {
            return 0;
        }

        return (int)$ret['time'];
    }

    /**
     *
     * @param int   $proto
     * @param array $C2S
     * @return array
     */
    protected function send(int $proto, array $C2S): array
    {
        $this->connect();

        $C2S = json_encode(['c2s' => $C2S]);
        $data = $this->encode($proto, $C2S);

        if (!$this->socket->send($data, strlen($data), MSG_DONTROUTE)) {
            throw new RuntimeException(Socket::strerror($this->socket->lastError()));
        }

        $recv = '';
        $buf = '';
        while ($rst = $this->socket->recv($buf, 2048, 0)) {
            $recv .= $buf;
            if ($rst < 2048) {
                break;
            }
        }

        $ret = $this->decode($recv, $C2S);

        if ($ret['proto'] && ($ret['proto'] != $proto) && !in_array($proto, [3001, 3002])) {
            throw new RuntimeException("proto Error:$proto - $C2S", 1);
        }

        return $ret['s2c'];
    }

    /**
     * 编码
     * @param int    $proto
     * @param string $C2S
     * @return string
     */
    protected function encode(int $proto, string $C2S): string
    {
        $body = $C2S;  // 默认不加密
        $ret = 'FT';
        $ret .= pack("L", $proto); //协议ID
        $ret .= pack("C", 1); //协议格式类型,0为Protobuf格式,1为Json格式
        $ret .= pack("C", 0); //协议版本,用于迭代兼容
        $ret .= pack("L", ++$this->sequence); //包序列号,用于对应请求包和回包
        $ret .= pack("L", strlen($body)); //包体长度
        $ret .= sha1($C2S, true); //包体原始数据(解密后)的SHA1哈希值
        $ret .= pack("@8");//保留8字节扩展
        $ret .= $body;

        return $ret;
    }

    /**
     * 解码回包
     * @param string $recv
     * @param string $C2S
     * @return array
     */
    protected function decode(string $recv, string $C2S): array
    {
        $head = substr($recv, 0, 44);
        $body = substr($recv, 44);

        $pack = unpack("CF/CT/Lproto/CProtoFmtType/CProtoVer/LSerialNo/LBodyLen", $head, 0);

        $ret = json_decode($body, true);
        if ($ret['retType'] != 0) {
            throw new RuntimeException("ret Error:{$pack['proto']}:{$C2S} - {$ret['retType']}:{$ret['retMsg']}", in_array($ret['retType'], [-1]) ? 4 : 0);
        }
        if ($ret['errCode'] != 0) {
            throw new RuntimeException("err Error:{$pack['proto']}:{$C2S} - {$ret['errCode']}:{$ret['retMsg']}", 1);
        }
        return ['proto' => $pack['proto'], 's2c' => $ret['s2c'] ?? ['result' => true]];
    }
}